package abacab.remote.protocol;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import abacab.ActorSystem;
import abacab.ext.SerializationExtension;
import abacab.ref.ActorRef;
import abacab.remote.Connection;
import abacab.remote.message.Message;
import abacab.remote.message.SerializedMessage;
import abacab.remote.ref.RemoteActorRef;
import abacab.remote.utils.RemoteUtils;
import abacab.serial.Serializer;

/**
 * Describes the encoding/decoding protocol for a message
 */
public class MessageProtocol {

	// **************************************************************************
	// **************************************************************************
	// ENCODING METHODS
	// **************************************************************************
	// **************************************************************************

	/**
	 * Encode a message
	 * 
	 * @param message
	 *          the message
	 * @param os
	 *          the output stream
	 * @param serializationExt
	 *          the serialization extension
	 * @throws IOException
	 *           if an error occurred
	 */
	@SuppressWarnings("rawtypes")
	public static void encode(Message message, OutputStream os, SerializationExtension serializationExt)
			throws IOException {
		if (message.getSender() != null) {
			writeString(message.getSender().getPath(), os);
		}
		else {
			writeString(null, os);
		}
		if (message.getRecipient() != null) {
			writeString(RemoteUtils.toLocalPath(message.getRecipient().getPath()), os);
		}
		else {
			writeString(null, os);
		}

		Object data = message.getMessage();
		if (data instanceof SerializedMessage) {
			os.write(((SerializedMessage) data).getSerializedForm());
		}
		else {
			Serializer serializer = serializationExt.getSerializerFor(data);
			writeInt(serializationExt.getIdForSerializer(serializer).intValue(), os);
			if (serializer.isClassNameNeeded()) {
				encodeClassName(data.getClass(), os);
			}
			serializer.encode(data, os);
		}
	}

	/**
	 * Encode the class's name
	 * 
	 * @param clazz
	 *          the class
	 * @param os
	 *          the output stream
	 * @throws IOException
	 *           if an error occurred
	 */
	@SuppressWarnings("rawtypes")
	public static void encodeClassName(Class clazz, OutputStream os) throws IOException {
		// TODO: have the possibility to add different implementations
		writeString(clazz.getName(), os);
	}

	/**
	 * Write a string
	 * 
	 * @param s
	 *          the string
	 * @param os
	 *          the output stream
	 * @throws IOException
	 *           if an error occurred
	 */
	public static void writeString(String s, OutputStream os) throws IOException {
		if (s != null && s.length() > 0) {
			os.write(s.getBytes(ActorSystem.CHARSET));
		}
		os.write(0);
	}

	/**
	 * Write an integer
	 * 
	 * @param i
	 *          the integer
	 * @param os
	 *          the output stream
	 * @throws IOException
	 *           if an error occurred
	 */
	public static void writeInt(int i, OutputStream os) throws IOException {
		os.write(i >> 24 & 0xFF);
		os.write(i >> 16 & 0xFF);
		os.write(i >> 8 & 0xFF);
		os.write(i & 0xFF);
	}

	// **************************************************************************
	// **************************************************************************
	// DECODING METHODS
	// **************************************************************************
	// **************************************************************************

	/**
	 * Decode a message
	 * 
	 * @param is
	 *          the input stream
	 * @param serializationExt
	 *          the serialization extension
	 * @param connection
	 *          the connection
	 * @return the message
	 * @throws IOException
	 *           if an error occurred
	 * @throws ClassNotFoundException
	 *           if the message's class was not found
	 */
	@SuppressWarnings("rawtypes")
	public static final Message decode(InputStream is, SerializationExtension serializationExt, Connection connection)
			throws IOException, ClassNotFoundException {
		String senderPath = readString(is);
		ActorRef sender;
		if (senderPath != null) {
			sender = new RemoteActorRef(RemoteUtils.toRemotePath(connection, senderPath), connection);
		}
		else {
			sender = null;
		}
		String recipientPath = readString(is);
		ActorRef recipient;
		if (recipientPath != null) {
			recipient = ActorSystem.findActor(recipientPath);
		}
		else {
			recipient = null;
		}

		int serializerIdentifier = readInt(is);
		Serializer serializer = serializationExt.getSerializerForId(Integer.valueOf(serializerIdentifier));
		Class clazz;
		if (serializer.isClassNameNeeded()) {
			clazz = decodeClassName(is);
		}
		else {
			clazz = null;
		}
		Object data = serializer.decode(is, clazz);
		return new Message(sender, recipient, data);
	}

	/**
	 * Decode the class's name
	 * 
	 * @param is
	 *          the input stream
	 * @return the class
	 * @throws IOException
	 *           if an error occurred
	 * @throws ClassNotFoundException
	 *           if the class was not found
	 */
	@SuppressWarnings("rawtypes")
	public static Class decodeClassName(InputStream is) throws IOException, ClassNotFoundException {
		// TODO: have the possibility to add different implementations
		String className = readString(is);
		return className != null ? Class.forName(className) : null;
	}

	/**
	 * Read a string
	 * 
	 * @param is
	 *          the input stream
	 * @return the string
	 * @throws IOException
	 *           if an error occurred
	 */
	public static String readString(InputStream is) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		int b;
		while ((b = is.read()) > 0) {
			baos.write(b);
		}
		if (b < 0) {
			throw new EOFException("Reached the end of the stream when expecting to read the class's name");
		}
		return baos.size() > 0 ? baos.toString() : null;
	}

	/**
	 * Read an integer
	 * 
	 * @param is
	 *          the input stream
	 * @return the integer
	 * @throws IOException
	 *           if an error occurred
	 */
	public static int readInt(InputStream is) throws IOException {
		return readByte(is) << 24 //
				| (readByte(is) & 0xFF) << 16 //
				| (readByte(is) & 0xFF) << 8 //
				| readByte(is) & 0xFF;
	}

	/**
	 * @param is
	 *          the input stream
	 * @return the byte read
	 * @throws IOException
	 *           if an error occurred
	 */
	public static int readByte(InputStream is) throws IOException {
		int r = is.read();
		if (r < 0) {
			throw new EOFException();
		}
		return r;
	}
}